React Suspense data fetching 探究

March 24, 2021

最近 React 16.6 中提出了新组件 Suspense 允许 React 挂起组件渲染直到 IO 的数据返回。这个特性在 JSConf Iceland 中 Beyond React 16 里面 Dan 就介绍到了,并在半年后的今天登陆 React 16.6。

Suspense 简单用法

在官网教程里面介绍到 Suspense 与 React.lazy 结合做 Code-Spliting ,自然是可以这么用的,只是这更多的是代码分割,除了代码分割以外 IO 的处理,在 Beyond React 16 演讲中还提到了 data fetching,Dan 的第二个 demo 主要提到的也是 data fetching。下面先看看 Suspense 的一个简单的非代码分割的 demo:

const Img = lazy(async () => {
  await delay(2000);
  return {
    default: ImageResource
  }
})

const ImageResource = props => <img {...props} />;

class App extends Component {
  render() {
    return (
      <div className="App">
        <Suspense fallback={<div>Loading...</div>}>
          <Img src="https://www.baidu.com/img/bd_logo1.png?where=super" />
        </Suspense>
      </div>
    );
  }
}

在官网教程提到 React.lazy 要动态的调用 import 方式注入组件,返回一个 Promise。 同时 Promise 返回一个 default 的组件。所以就可以采用上面的方式,而不必用 import 方式,只是如此还是类似于代码分割,没有达到数据获取返回之前挂起组件的思路。

Dan 的例子

在 Dan 的演讲里面多处地方有用到 Suspense 的意识,也就是 createFetcher 与 this.deferSetState 方法。deferSetState 通过异步修改组件显示与否,当 createFetcher 传参 Promise 状态结束后才执行异步 deferSetState,显示已经接收到数据的组件。这就是挂起 Suspense 的作用了。在 React 16.6 里面没有提供 deferSetState,可能也是更多的异步操作留到 17.0 大版本上。而 Suspense 组件,已经可以实现 children 的挂起行为了。所以可以猜测,Suspense 组件作用就是 this.deferSetState

Dan 的 demo 里面,采用的 createFetcher 方法,类似于 simple-cache-provider 包 ,提供数据缓存控制。由于 Dan 的 demo 里的获取数据函数 Promise 没有写明具体的实现方式,不知道里面有没有什么巧门,还好在 demo 最后里有提到图片更新的 Promise,具体代码如下:

const imageFecther = createFether(
  (src) => new Promise(resolve => {
    const image = new Image();
    image.onload = () => resolve(src);
    image.src = src;
  })
)

function Img(props) {
  return (
    <img 
      {...props}
      src={imageFecher.read(props.src)}
    >
  )
}

这整个组件 Img 还是很简单的,若是平时的直接将 Promise 返回值到 img 标签的 src 将得到的是 [Object Promise],而不是 url。只是为何凭借一个 resolve,Suspense 组件就知道可以有数据进来,可以解除挂起状态,并让 src 得到想要的 url 嗯?

由于 simple-cache-provider 包已经不再更新而且在 react 项目里面找不到了,所以单独下载 simple-cache-provider 包。这个缓存控制包,代码在300+,主要 api 有 createCache、createResource 和 SimpleCache。上面 demo 中的代码采用 simple-cache-provider 包实现,就要用到 createCache 来创建缓存,createResource 来获取资源。简化一下 simple-cache-provider 包,替换 createCache 以及 createResource,其具体实现如下:

const cacheResourceSimple = {};

const createResourceSimple = function (miss) {
  return (resource, key) => {
    if (!resource[key]) {
      resource[key] = {
        key,
        value: false,
      }
      const _suspender = miss(key);
      _suspender.then((value) => {
        resource[key].value = value;
      });
      throw _suspender;
    } else {
      return resource[key].value;
    }
  }
}

const imgFetcher = createResourceSimple(
  src =>
    new Promise(resolve => {
      const img = new Image();
      img.onload = () => resolve(src);
      img.src = src;
    })
)

function Img (props) {
  return (
    <img 
      {...props}
      src={imgFetcher(cacheResourceSimple, props.src)} 
    />
  )
}

图片组件的挂起表现正常,基本上就实现 Dan 的挂起图片的功能了。

只是上面的代码为什么可以实现图片组件的挂起呢?明明 imgFetcher 函数返回的也是一个 Promise,最后 src 将得到的还是 [Object Promise] 呀。抱着这样的疑惑,打断点试了试,发现上面代码神奇之处在于 throw _suspender。在 simple-cache-provider 包里面的就是这样实现的,执行 _suspender 之后抛出错误。

在这里就要回到 react 源码了,当 throw _suspender 的时候,会被 renderRoot 方法捕获 catch ,进入 throwException 抛错函数来处理,而 throwException 里面 会判断 fiber 是否是 Symbol(react.suspense) 类型,并且挂起的组件非空,则会执行下面。

thenable.then(onResolveOrReject, onResolveOrReject);
var nextChildren = null;

简单的就是挂了个 then。当图片 resolve 之后,会执行 _suspender.then,接下来又会执行 thenable.then ,从而使得该组件在有数据返回的时候立刻异步渲染。后面的 nextChildren 置空,则是使得组件在数据没有返回前的渲染好像只是渲染 null 一样。具体 react 实现是复杂的,一环接一环的,这里就不做具体分析了。但是 抛出 createResourceSimple 的传参 Promise 给到 react 捕获到 ,才有了 suspense data fetching 的行为。

总结

实现 Suspense 组件的另外一个更大的作用 suspense data fetching 的方法,只需要将 IO 接口请求的异步函数当作错误抛出,就可以了。官网里面没有介绍到这个方式,可能也是考虑到功能还不完善?丑媳妇迟早要见公婆的。

参考

  1. Beyond React 16 Dan 的这个视频,看到前面的 CPU time slicing 就兴奋不已了,倒是忘记后面新功能。